昨天講的是,我能把同一個 HTTPS 請求,從線上的加密封包一路對到解密後的 HTTP 內容,靠的是我能控制客戶端的執行環境、封包擷取點與 TLS key log。
但這套能力仍綁在一台由我控制的機器上。
那台機器就是問題本身。
前面二十三天,我處理的是一個審查員在自己的機器上怎麼工作、怎麼被評測、怎麼被觀測與限制。接下來五天,問題換成:換一個人從瀏覽器開場時,審查員的執行環境、網路邊界、流量紀錄與遙測,能不能照樣成立。
| 第幾層 | 層次 | 回答的問題 | 對應內容 |
|---|---|---|---|
| 1 | Skill | 它應該怎麼判斷? | 流程、九面向、Pushback 與報告 |
| 2 | 評測 | 怎麼知道它沒有變壞? | 行為 Eval、Benchmark 與規則退役 |
| 3 | 能力邊界 | 它實際拿得到什麼? | Container、Proxy、ssh-agent 與 Firewall |
| 4 | 觀測 | 它實際做了什麼? | Trace、Transcript、MITM、封包與計數 |
| 5 | 平台 | 怎麼讓以上成為每一場的預設? | 一場 Session 一顆容器、控制平面與對帳 |
第五層不是多做一個 UI,而是把前四層從我的操作習慣,變成每一場 Session 的預設。主語仍是同一個審查員;只是如果那些邊界只有我坐在那台筆電前、照正確順序回答選單才成立,它們就還不是系統能力,只是我的個人儀式。
今天我在瀏覽器按下「建立 Session」,那一列卻一直停在「建立中」。沒有錯誤訊息,也沒有終端可以進;從外面看起來只像是比較慢,實際上容器裡的啟動腳本正卡在一道 read,等一個服務永遠不會送進去的鍵盤輸入。
前四層累積下來的能力,現在得由一個沒有鍵盤的服務自動叫起來。當時有兩條路:在控制平面裡重寫一次那些步驟,或讓原本的腳本也能接受環境變數。我選後者,因為能力的實作應該只有一份;但這次失敗也讓我看見,即使沒有重寫邏輯,只要控制平面另外養了一套變數名稱,還是等於多了一份需要同步的語彙。
控制平面送進容器的,是它自己取的變數名,不是腳本原本認得的那一組。兩邊的對照一漂掉,選單就不會被跳過,啟動流程也不會報錯,只會停在那裡等輸入。
同樣的形狀後來又出現一次。我加了「錄製範圍」這道題,卻忘了把對應的環境變數一起送進去。一樣沒有錯誤,一樣只剩畫面上的「建立中」。
這類錯最麻煩的地方,是它不會失敗給你看,只是不往下走。養兩份同義的名字,付的就是這種代價。因此後面的分工寫死成一句:能力的做法留在原本那份腳本裡,控制平面只決定要不要。
我要的是這件事:不管手上是哪一台裝置,打開瀏覽器,開一場環境出來,跑完一套 Code Review,再終止這場 Session。環境每次都一樣;Session 容器的可寫層會被丟棄,對話紀錄則留在容器外面的使用者空間,下次仍能接回來。

用途不只這一件,日常開發也能在裡面做,要擴充也擴充得上去。我沒有在它外面再包排程或工作流編排;Claude Code 已有自己的排程與自動化機制,這篇只處理互動式 Session。
平台支援的協作範圍有一條明確邊界:它支援多個彼此隔離的使用者,但不支援多人共同操作同一場 Session。 各自登入、各自的空間、各自的憑證都做了;沒做的是同一場裡兩個人一起看、一起打字。身分與 Session ownership 並不是等到「要開放給誰」才需要處理:只要有登入畫面,就得回答誰是誰、誰的東西歸誰。做了也不等於它就能放心租給互不信任的人用;身分、Session ownership 與營運者信任是三個不同問題。
平台一登場,主角看起來會像是換成另一套東西。但每一道設計回答的都是同一個問題:一個會自己選路徑、而且我不完全信任的東西住進來之後,誰約束誰。 前四層那些能力也是同一個問題的答案,只是那時候它跑在我的筆電上,沒有人需要問「誰能開一場」。因此最後還得回頭驗一次這些約束是不是真的立住,再把整段路收起來。
照著這些原則實作,長出來的未必是我這一套;這本來就是預期結果。這不是一篇逐步照抄的 Web Terminal 教學;完整實作放在公開 repo nathanfhh/30-days-ironman 的 claude-pty/,控制平面、對帳程序、資料庫 schema 與各版本的 ADR 都在那裡。文章留下的是為什麼需要這條邊界、有哪些選項、我怎麼選、哪個假設後來被推翻,以及我拿什麼驗收。真正能搬走的是這些問題,不是我當時挑的每一項工具。

https://www.youtube.com/watch?v=LJwRVK3R16I
一場 Session 的真身是一顆容器;容器之外,平台把責任拆成四塊,各自只做一件事:
它的 PTY 由 Docker Daemon 持有,容器的主程序則承載這一場 CLI。Session 的生命週期因此可以整個交給 Daemon 管,我要做的只是 stop 跟 rm。
PID 1 的實作註記
未開流量錄製時,entrypoint 會用
exec讓 Claude Code 接管 PID 1;開啟錄製時,entrypoint 本身留在 PID 1,Claude Code 是它的子行程,這樣才能轉送終止訊號,並在 CLI 結束後收掉 mitmproxy、寫完附檔。兩條路都把同一場 CLI 綁在容器的主生命週期上,但不能一概說 Claude Code 永遠是 PID 1。
這個設計最省事的地方就在這裡。容器與主程序的生命週期由 Daemon 管理,我這邊跟它的耦合只剩下一個容器代號,不必另外維護一份「誰還活著」的程序狀態。瀏覽器連線中斷時,原本的容器與 PTY 仍可繼續存在;但主程序被終止或 Daemon 本身重啟時,原本那一場就不再存在。
Daemon 重啟是這套設計沒有處理的一條邊界。升級 Docker 或重啟服務的時候,除非事先開了 live-restore,執行中的容器會跟著一起走,所有 Session 當場斷頭。我沒有防這件事,能接住它的是下面會講的另一層:已經落盤的對話紀錄寫在從外面掛進去的目錄裡,不在容器的可寫層;重新開場後,仍可再用 /resume 找回。
Daemon 重啟時,原本那一場已經終止;一般的瀏覽器斷線則不同。這時我要接回的不是同一顆容器裡另一個 shell,而是剛才那條仍在執行的 CLI。這正是 attach 與 exec 的分界。
docker run -itd 建立的 PTY 由 Docker Daemon 持有,attach 接回的是容器建立時那條主程序路徑的標準輸入輸出與同一條 PTY;exec 則是在既有容器中另外啟動一個程序。後者就算也能做成長時間執行,語意仍不是「回到原本那一場」。
所以規則寫死成一句:所有客戶端一律走 attach,不走 exec。
當初動工前先做了一輪實測,決策紀錄跟程式碼一起放在 repo 裡。同時開多條 attach 連同一場,彼此互相鏡像,輸入回顯跟執行結果都一致。全部斷開之後,容器裡的程式繼續跑,重連回去可以接著操作。Ctrl+C 這類 raw byte 經 attach 原樣穿透,訊號語意正確,中文輸入也沒有失真。
換句話說,關掉瀏覽器再回來,還是同一場。這是我要的持久性;我借 Docker 管住容器主程序與 PTY 的生命週期,不必自己再養一套 Session daemon。
這套持久與重連契約有一條邊界: 它只涵蓋由 entrypoint 啟動、綁在容器主生命週期上的那一場 CLI。臨時用 exec 另外開的 bash 不在契約內,我不替它提供恢復保證。
Daemon 幫我保管的是那條 PTY,不是「畫面現在長什麼樣」。重新 attach 上去,只會從接上的那一刻開始收到新的 byte,不會重播先前畫面。先前送到 stdout/stderr 的內容,是否還能從 docker logs 取回,則取決於 logging driver;那是另一套紀錄,不是 PTY 的捲軸緩衝。
在我當時的 logging 設定下,拿一顆印完兩百行就去睡覺的容器實測,docker logs 兩百行都在,重新 attach 收到的仍是零。
這件事我國中上資訊課時就遇過。當時學校教的是 Visual Basic 6.0;我在表單上畫圖時,只要把視窗拖出螢幕外再拖回來,移出畫面的那一塊就整個不見了。後來才知道有一個屬性叫 AutoRedraw,預設是關的:關著的時候,畫出來的東西只寫進螢幕,沒有留在記憶體裡,視窗被蓋住或移出去,那部分就沒了,除非自己在重繪事件裡再畫一次。打開它,系統會多維護一份離螢幕的點陣圖,需要重畫時自動貼回去。
十幾年後,我在終端重連上遇到的是同一個問題:畫面如果沒有另外保存,重新接上時就沒有東西可以直接畫回來。 要補回重連前的畫面,大致有兩條路。一條是把 logging driver 記下的輸出取回來重播;另一條是在伺服器上跑一套終端模擬,持續維護「現在螢幕上是什麼」,重連時再用那份狀態重建畫面。後者就是把 AutoRedraw 打開的做法。
這兩條路我都沒有採用。
重播那條路直接放棄。一場跑很久的 Session,原始輸出的量非常可觀,從頭放一次成本太高;只放尾巴又會從跳脫序列中間切開,畫面會花掉。維護螢幕狀態那條路則是要為此養一套終端模擬,而我的互動對象自己就會重繪。
我原本的推論是:終端的捲軸緩衝住在瀏覽器那一端,重新連線等於拿到一個全新的空緩衝,因此往上捲理應什麼都沒有。真正用起來才發現,這個推論只對了一半:緩衝確實是新的,實際往上捲卻看得到先前的內容。
當時把內容重新印進新緩衝的,是那個版本的 Claude Code,不是 ttyd,也不是伺服器重播。
為了釐清這件事,我請 Claude 做了一輪技術驗證(spike):先不做正式功能,只用最小實驗把重連鏈路量清楚。這裡得拆開兩件事:平台怎麼讓容器裡的程式收到尺寸變化,以及 Claude Code 收到之後怎麼反應。
現行平台不是靠 docker attach 把視窗尺寸送進容器。瀏覽器先把終端算出的 rows/cols 送到 /resize,控制平面再呼叫 Docker 的 container resize API 改容器 PTY 的尺寸。按照 Linux 的規則,PTY 尺寸真的改變時,核心會把 SIGWINCH 送給前景行程群組(foreground process group)。我也另外請 AI 用一顆只記錄 signal 的最小容器重做過:不送任何輸入,只連續改兩次尺寸,記錄裡收到兩次 SIGWINCH。
接下來才是版本相依的觀察。當時那輪 spike 裡,我沒有送按鍵;尺寸改變後,Claude Code 清掉畫面、把游標移回原點,再把它當時握著的內容重新印出來。重印的不只是當下那一格,先前完成的多輪內容大多也跟著回來。我能確定的是,那個版本的 Claude Code 在尺寸變化後重新輸出了畫面;我沒有它的內部實作或公開契約,不能把這件事寫成每個版本都一定會重繪。
在我目前測到的環境裡,先前完成的幾輪內容大多會重新出現;但可回畫的範圍不是平台保存的資料,也沒有 Claude Code 的公開介面保證,可能隨電腦、終端尺寸、Claude Code 版本與當下對話狀態而不同。
因此,那次往上捲看到的字,不是平台重播 PTY 歷史,而是 Claude Code 在 resize 後重新輸出,落進全新的瀏覽器緩衝,量夠多就撐出捲軸。它仍然很像 AutoRedraw,只是這次負責重畫的不是作業系統,而是當時版本的應用程式。
那輪 spike 還測到另一個條件:尺寸沒有實際改變時,我沒有觀察到立即重繪。 把新的 pty 開成跟舊的完全一樣大,容器裡收不到 SIGWINCH,畫面也沒有立刻重繪。這是當時版本的實作行為,不是 Claude Code 承諾的介面;更新版本後可能改變。當時量到,會自己刷新的畫面大約 2 秒回來,等輸入的畫面則在按一個鍵後整頁重繪。
現行版本的代償
這個觀察後來變成平台的一段保險。若瀏覽器回報的列數與欄數跟資料庫記下的上一筆相同,伺服器會先把寬度暫時縮一欄,再還原成目標寬度,主動製造兩次實際的 resize;在這次驗證的 Linux/Docker 環境裡,這會讓前景行程群組收到
SIGWINCH。它補的是觸發重繪的條件,不是完整的終端歷史;應用程式會不會重新輸出、又會輸出多少,仍取決於當時版本的實作。
所以重連的語意比我原本以為的好,但也只好這麼多:在我目前測到的版本與環境裡,通常拿得回 Claude Code 仍能重新輸出的那一段,不是這場 Session 從頭到尾的終端畫面。 沒有被應用程式重印的畫面,這套機制不會替我補回;已經落盤的 Claude Code 對話紀錄則寫在容器外面的使用者目錄裡,在持久化未關閉、掛載仍完整的前提下,重新開場後可再用 /resume 找回。
留下的是對話紀錄,不是完整畫面。
剩下那一段我還是沒有去補。要讓捲軸完整接得回去,就得在伺服器上養一套螢幕狀態,那份東西要一直跟著跑、一直維護,而它讓人多拿到的,是一個比較順手的動作而已。
前一張圖畫的是 Session 容器外面的四塊平台責任。現在把 Session 容器與 ttyd 放回來,看資料實際走哪裡。Nginx 是唯一對外入口;Flask 控制平面、對帳程序與 SQLite,分別負責決策、收斂與仲裁。
在正常的使用者請求路徑裡,只有控制平面接受建立與終止 Session 的決定。用哪顆 image、哪個工作目錄、掛什麼進去、一個人最多開幾場,全部由它決定,前端不能指定命令。
對帳程序技術上拿著同一顆 Daemon 的權限,因此也是特權元件;只是它不接受使用者送來的命令,也不負責建立新的 Session,而是定期把帳面校正回現實,清理失效的 view、孤兒與逾時資源。它的故事比較長,今天先擱著。
資料在這個系統裡走兩條路,從 Nginx 分開後便不再合流。
控制面:瀏覽器到 Nginx,Nginx 到控制平面,控制平面經 Docker socket 操作 Daemon。開一場、關一場、列出現在有幾場,走這條。
資料面:瀏覽器到 Nginx,Nginx 到 ttyd,ttyd 經 attach 接上那條 PTY。你在終端上敲的每一個鍵、畫面上跑出來的每一個字元,走這條。
畫成圖大概是這樣:

ttyd 跟控制平面其實住在同一個容器裡,但那不影響上面的分界:走的是不同的路,經手的東西也不同。

分開的好處很實際。控制平面維持短請求的形狀,開完一場回一個 id 就結束了,長壽的 byte 流留在 ttyd 跟 Daemon 那邊,不佔它的 worker。
而這道分界線的位置,剛好就是授權必須掛上去的地方。那條 byte 流一旦接起來,中間就不會再回頭問任何人,所以「這一場是不是你的」這個問題,必須在 Nginx 把連線交給 ttyd 之前就問完。這個判斷成立還有一個不可省略的前提:ttyd 的 port 不能直接發布到 Host。這一版只讓它存在於內部 Docker network,Session 容器也沒有接上這張平台內部網路;外部唯一能走的入口仍是 Nginx。否則不論從 Host 或不受信任的 Session 直接碰到 ttyd,前面那道 ownership 檢查都可以被整條繞過。授權必須從這道分界開始。
三層,各自負責不一樣的東西。
資料庫是唯一的仲裁者。 誰擁有哪一場、動態的 port 分給了誰、配額算到誰頭上,全部走資料庫交易。控制平面不保留權威的記憶體狀態;多個 Flask worker 會碰到的配額、port 與讀後寫路徑,則由資料庫交易、唯一約束,以及 repo 裡的 test_persistence.py 與 test_mutex_semantics.py 併發測試守住。
每個人自己的那塊空間放狀態。 在持久化未關閉、內容已經落盤的前提下,Claude Code 的 session 資料 會寫進設定目錄,而那個目錄是從外面掛進去的。容器被殺或控制平面崩潰,只要這塊掛載仍完整,已落盤的對話紀錄就還在。即使 SQLite 整個遺失,transcript 檔案也不會跟著消失;但帳號、Session ownership、port 與配額等控制平面狀態仍得另外重建。只要重新開出的容器掛到正確使用者的設定目錄,工作目錄也相同,就能用 /resume 接回既有對話。
工作目錄為什麼也要對得上?因為在我當時使用的 Claude Code 版本裡,那些紀錄是按工作目錄分桶存的。/resume 列可以接的對話時,它是拿「我現在站在哪個目錄」去找對應的那一份。所以掛對目錄還不夠,得站在同一個位置上問。
容器只負責執行。 寫在容器可寫層裡的東西,關掉就沒了。
在資料庫與掛載於容器外的使用者空間仍完整的前提下,Session 的生命週期 bug 多半會從「對話遺失」降級成「資源衛生」:壞掉時可能少一顆容器,或多一顆殘留容器,但不會因為容器被刪除,就連已經落盤的 transcript 一起消失。資料庫本身遺失則仍是控制平面的復原事件,不能只當成清孤兒容器。
到這裡都還在講骨架。但這套東西真正要接住的,是前四層累積下來的能力。
那顆開發容器的啟動腳本裡,有一整套按順序做完的事:套防火牆白名單、起錄流量的 proxy、設定 Telemetry 的匯出端點。這些邏輯充滿踩過坑才長出來的細節,順序錯一步就有東西默默不生效。
而它原本是問人的。啟動時跳四個選項,人在鍵盤前面回答。
一個服務沒有鍵盤可以按。要讓它自動開場,最直覺的做法是在控制平面裡用 Python 重寫一遍那些步驟。我沒有這樣做,因為那等於把同一套帶疤的邏輯養成兩份,日後每改一次都要記得改兩邊,而且忘記的那一次不會有人發現。
做法是把那些選項改成可以用環境變數回答。有給值就用它並跳過詢問,沒給就照原樣問人。這裡有一條紀律寫死:必須是嚴格加法,環境變數一個都沒設的時候,腳本的行為要跟改之前逐字元相同。因為人還是會直接開容器來用,那條路徑一個字都不能動到。
於是分工變成這樣:能力的做法留在腳本裡,控制平面只決定要不要。控制平面補的是環境變數給不了的東西,例如加上網路權限、把容器接到哪個網路、掛什麼進去。
開頭那顆停在「建立中」的容器,就是這個決定沒被貫徹的那一次。
這裡有一個具體的數字:在我當時的部署環境、Trivy DB 已命中快取時,限制模式相較於開放模式,開場大約多 1 秒;其中套用防火牆規則約佔半秒。這是單一環境的實測,不是啟動時間保證。安全的預設值應該是限制而不是開放,所以這一秒我不打算省。
這一節的三個小節其實在回答同一件事:控制平面要建立與終止 Session,對帳程序要收斂帳面與現實,兩者都必須操作 Docker Daemon。那份權力縮不掉;下面三個小節分別看它換到什麼、又留下什麼。
控制平面自己也是容器,而它必須有建立容器的權力。取得 Daemon 控制權有兩條路。
一條是把 Host 的 Docker socket 掛進控制平面容器,操作外面那顆 Daemon。這樣 Session 容器是控制平面的兄弟,不是它的子容器。
另一條是在控制平面容器裡再跑一個 Daemon,Session 建在內層那顆上。
我選了第一條,但不是因為第二條比較複雜。
第一,內層的 Daemon 看不到外面的東西。 限制模式(只放行白名單的那個網路模式)要把 Session 接上代理那個網路,Telemetry 要連得到 Jaeger(收 trace 的那顆容器),這兩樣都是外層 Daemon 上的資源。在內層重建整套防火牆、代理跟 Telemetry,成本完全不成比例。
第二,它沒有提供一條我願意依賴的安全邊界。 我當時評估的傳統 DinD 做法,需要讓內層 Daemon 跑在 --privileged 容器裡。這會給容器所有 Linux capabilities、主機裝置存取,並卸掉多項預設隔離;Docker 官方也明確警告,這不是安全的 sandbox,可能取得 Daemon 所在主機的控制權。DinD 與掛入 Host socket 的攻擊面並不相同,但兩者都會讓控制平面成為高權限元件,因此我不把 DinD 當成這裡的安全收益。
第三,掛載路徑是 Daemon 在解讀的。 這套東西大量依賴從外面掛進去的路徑,憑證是其中之一。走內層的話,那些路徑在內層不存在,得再搬一層,而且憑證會落進一個 privileged 容器裡面。
在這版使用 rootful Docker 的部署裡,把 socket 掛進去,約等於給那個容器 Host root。
能建立容器就能建立一個 privileged 的容器,把整顆根目錄掛進去。這件事不能靠「把控制平面也容器化」來消除,因為控制平面本來就必須有建立容器的權力,它天生是特權元件。
我在決策紀錄裡寫了一句話當提醒,怕自己日後看到它在容器裡就安心:容器化買到的是部署一致性,不是安全邊界。
所以有一條紅線寫在那裡不准動:Session 容器一律不掛 Docker socket。裡面跑的是會執行不受信任程式碼的 AI,給它 socket 等於整套隔離直接放棄。Day 20 那道防火牆的收尾是「預設不給,需要什麼再單獨開」,這條紅線是同一句話換一層。SSH agent 轉發同理,維持預設不掛,要用時明確開啟。它沒有讓爆炸半徑消失,只是改變了憑證交付形式,Day 19 已經講過。
控制平面容器裡的程序是以非 root 執行的。非 root 在這裡有效的範圍很窄: 它不會讓上面那段話失效。只要程序仍能操作 Docker socket,就能要求 Daemon 建立高權限容器。非 root 在這裡是縱深防禦,不是邊界;它限制的是應用程式在目前容器裡,直接讀寫受檔案權限保護的內容,以及執行未被授予之 capabilities 所需操作的能力。
而這個選擇有兩個實測踩出來的前提。第一,Docker socket 的存取權由 Host 上那顆 socket 實際的 UID、GID 與 mode 決定,不是固定的 root:root。非 root 程序必須取得相符的群組權限,而 GID 在不同環境可能不同,所以部署設定必須允許覆寫。第二,既有的資料目錄不會有人替你把擁有者改對,升級舊的部署得手動改一次,不然資料庫連檔案都開不起來。
兩個都不難,但都是那種「照著文件做卻起不來,而錯誤訊息不會告訴你原因」的東西。
還有一個實作上的坑,是這個選擇直接帶來的。控制平面看到的檔案系統跟 Daemon 看到的不是同一個。它用自己的位置推導出來的路徑送給 Daemon,Daemon 是拿去 Host 上找的,結果不是掛載失敗,就是更糟的那種:靜默建出一個空目錄,容器照樣起來,只是缺了本該掛進去的東西,而且沒有人會報錯。所以「給 Daemon 的 Host 路徑」跟「自己檢查存在性的路徑」必須分成兩組變數,不能共用一個。
直接降低 Docker Daemon 在 Host 上權限的一條路,是改用 rootless mode。Daemon 與容器都以非 root 使用者執行;即使這顆 socket 被控制,權力上限也不再直接等同 Host root。不過攻擊者仍可能控制該使用者名下的容器與資源,所以它是縮小爆炸半徑,不是把風險歸零。
這件事還沒做。理由不特別:先讓東西能用,優化留到後面一輪。它是待研究的項目,不是我評估完決定不做的東西,這兩者的差別在於前者會被排進待辦,後者不會。
但「還沒做」不等於就這樣算了。在它補上之前,還有一層能補,而且不用改任何一行程式:把它跑在一台獨立的主機上,不要跟別的服務混住。再保守一點,整台包進一個虛擬機裡,外面再多一道邊界。
這是同一個問題的另一種解法。程式裡縮不掉的權限,就用它跑在哪裡來收。控制平面天生是特權元件,那就讓它待在一個「就算整台被拿走,拿走的也只有這一台」的地方。
另外兩個常被提到的做法,我先分開看。控制平面保持最小、不在裡面跑不受信任的東西,是必要的紀律,但不能代替權限邊界。這一版也沒有在 Docker socket 前部署 proxy 或 authorization plugin,因此控制平面拿到的仍是完整的 Daemon 權力。
一個只按 endpoint 放行的簡單 socket proxy,確實可能在允許 containers/create 之後,連特權容器也一起放過;但這不是授權層做不到。Docker 的 authorization plugin 可以檢查 method、URI 與 JSON request body,做更細的拒絕規則。只是我目前沒有實作、測試或依賴這層保護,所以不把它算進現有安全邊界。
把一套只在自己機器上跑的能力搬到瀏覽器後面,架構上的決定其實不多:Session 的真身是容器、只 attach 不 exec、在實測版本裡重連拿回的是應用程式重新輸出的那一段、控制面跟資料面分兩條、狀態放在容器外面、能力的做法留在原本那份腳本裡、控制平面與對帳程序拿 Host 的 socket 並承認自己是特權元件。
真正變的是性質。它一上網路就成了服務。即使最初只有我一個使用者,也得先回答誰能打開它,以及每個人能碰到哪一場。
明天從第一道門開始:誰能打開這個 AI Shell?登入只是第一關,後面還有 Session 的擁有權。